﻿using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Configuration;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.ServiceModel;
using Contracts.ClientTools;
using Contracts.ServiceContracts.ClientCache;
using Microsoft.ApplicationServer.Caching;
using Types.Constants;
using Types.Enumerations;

namespace Contracts.Caching
{
	internal class AppfabricCacheProvider : ICacheProvider
	{
		public static AppfabricCacheProvider GetProviderInstance
		{
			get { return new AppfabricCacheProvider(); }
		}

		private static ConcurrentDictionary<string, DataCacheItemVersion> _versionCache = null;
		private static Hashtable _cacheItemHashTable = null;

		private static DataCacheFactory _factory = null;
		private static DataCache _cache = null;


		static AppfabricCacheProvider()
		{
			#region Appfabric Initialization Section
			/// DataCacheClientLogManager.ChangeLogLevel(TraceLevel.Off) performans için eklendi
			DataCacheClientLogManager.ChangeLogLevel(TraceLevel.Off);

			_factory = new DataCacheFactory();

			string appFabricCacheName = "AppFabCache";
			if (ConfigurationManager.AppSettings.AllKeys.Contains("appFabricCacheName"))
				appFabricCacheName = ConfigurationManager.AppSettings["appFabricCacheName"];

			_cache = _factory.GetCache(appFabricCacheName);


			//Used to add cache versio informations.
			_versionCache = new ConcurrentDictionary<string, DataCacheItemVersion>();

			#endregion

			#region DataSet Cache List, Store Configuration

			_cacheItemHashTable = new Hashtable();

			var lastCacheName = string.Empty;
			try
			{
				foreach (CacheItem ci in CacheProvider.ProviderSettings.CacheItems)
				{
					_cacheItemHashTable.Add(ci.CacheItemName, ci);
					lastCacheName = ci.CacheItemName;
				}
			}
			catch (Exception exc)
			{
				string msg = string.Format("Dublicated Cache Item: '{0}'", lastCacheName);
				EventLog.WriteEntry(CommonConstants.EventLogSource.Application, msg, EventLogEntryType.Error, CacheEventLogId.DoublicatedCacheItem);
				throw new ApplicationException(msg, exc.InnerException);
			}
			#endregion
		}

		#region DataSet Cache

		private static DataSet AppInsertCache(string key, CacheItem pCacheItem)
		{
			if (pCacheItem.CacheType == EnumCacheType.ClientCache)
			{
				return ServiceProxyCreator<IClientCache, DataSet>.CallService(proxy => proxy.GetCacheItem(key));
			}
			else
			{
				//Check from cache before get
				DataSet returnDS = StaticGetCacheAndUpdateVersion(key);

				if (returnDS != null) return returnDS;

				Assembly asm = Assembly.GetAssembly(Type.GetType("Services.BusinessObjects.ClientCache.ClientCacheBS, Services"));

				try
				{
					Type bsType = asm.GetType("Services.BusinessObjects.ClientCache.ClientCacheBS");
					object mBSServiceCacheHelper = bsType.InvokeMember(null, BindingFlags.CreateInstance, null, null, null);
					returnDS = (DataSet)bsType.InvokeMember("GetDataForCache", BindingFlags.InvokeMethod, null, mBSServiceCacheHelper, new object[] { pCacheItem.CacheItemName });
				}
				catch (TargetInvocationException tEx)
				{
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, tEx.ToString(), EventLogEntryType.Error, CacheEventLogId.CacheError);
					if (tEx.InnerException != null)
					{
						throw new FaultException(new FaultReason(tEx.InnerException.Message));
					}
					else
					{
						throw new FaultException(new FaultReason(tEx.Message));
					}
				}
				catch (Exception exc)
				{
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, exc.Message, EventLogEntryType.Error, CacheEventLogId.CacheError);
					throw new FaultException(new FaultReason(exc.Message));
				}

				//Check from cache before insert
				DataSet returnDSLatest = StaticGetCacheAndUpdateVersion(key);
				if (returnDSLatest != null) return returnDSLatest;

				InsertCacheToAppFabric(key, pCacheItem, returnDS);
				return returnDS;
			}
		}

		private static void InsertCacheToAppFabric(string key, CacheItem pCacheItem, DataSet returnDS)
		{
			if (pCacheItem.MinutesToExpire <= 0)
			{
				try
				{
					DataCacheItemVersion version = _cache.Put(key, returnDS);
					_versionCache[key] = version;

					string msg = string.Format("Cache Inserted Without Timeout: {0}", key);
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, msg, EventLogEntryType.Information, CacheEventLogId.CacheFilled);

				}
				catch (DataCacheException dcExecption)
				{
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, string.Format("{0} | {1}", dcExecption.Message, key), EventLogEntryType.Error, CacheEventLogId.CacheError);
					throw new FaultException(new FaultReason(dcExecption.Message));
				}
			}
			else
			{
				try
				{
					TimeSpan ts = DateTime.Now.AddMinutes(pCacheItem.MinutesToExpire) - DateTime.Now;
					DataCacheItemVersion version = _cache.Put(key, returnDS, ts);
					_versionCache[key] = version;

					string msg = string.Format("Cache Inserted With Timeout: {0}", key);
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, msg, EventLogEntryType.Information, CacheEventLogId.CacheFilled);
				}
				catch (DataCacheException dcExecption)
				{
					EventLog.WriteEntry(CommonConstants.EventLogSource.Application, string.Format("{0} | {1}", dcExecption.Message, key), EventLogEntryType.Error, CacheEventLogId.CacheError);
					throw new FaultException(new FaultReason(dcExecption.Message));
				}
			}
		}

		private static DataSet StaticGetCacheItem(string key)
		{
			DataSet dsi = StaticGetCacheAndUpdateVersion(key);

			if (!_cacheItemHashTable.ContainsKey(key))
			{
				throw new ApplicationException("Cache key not exist in config file.");
			}

			if (dsi == null)
			{
				lock ((CacheItem)_cacheItemHashTable[key]) // locking can be done with appfabric getandlock / putandunlock
				{
					//Let's check. Another cache client may have added
					dsi = StaticGetCacheAndUpdateVersion(key);
					if (dsi == null)
					{
						dsi = AppfabricCacheProvider.AppInsertCache(key, (CacheItem)_cacheItemHashTable[key]);
					}
				}
			}

			return dsi;
		}


		private static DataSet StaticGetCacheAndUpdateVersion(string key)
		{
			DataSet dsi = null;
			DataCacheItemVersion dcvi = null;

			//When we use Get method, it is getting last version.
			//So, we need to compare with the version we have
			dsi = _cache.Get(key, out dcvi) as DataSet;

			if (dsi != null)
			{
				UpdateVersionCache(key, dcvi);
			}
			return dsi;
		}

		private static void UpdateVersionCache(string key, DataCacheItemVersion dcvi)
		{
			//we need to compare with the version we get by using Get method
			if (_versionCache.ContainsKey(key))
			{
				if (dcvi.CompareTo(_versionCache[key]) == 0)
				{
					//Same version. Do nothing
				}
				else
				{
					//Different version
					_versionCache[key] = dcvi;
				}
			}
			else
			{
				//Never demended before.
				_versionCache[key] = dcvi;
			}
		}

		private static DataSet StaticGetCacheItemIfNewer(string key)
		{
			DataSet dsNewer = null;

			//If there is no any information in my version cache, this information should be added
			if (!_versionCache.ContainsKey(key))
			{
				dsNewer = StaticGetCacheItem(key);
			}
			else
			{
				//If there is version information, it should be compared bu using GetIfNewer method
				DataCacheItemVersion dcvi = null;

				dcvi = _versionCache[key];
				dsNewer = (DataSet)_cache.GetIfNewer(key, ref dcvi);


				//If object is null, it is already last version and so I can read it
				if (dsNewer != null)
				{
					// There is no need to control the version. Because, already new version came from server side
					// Let's return back this and add version number to local version cache
					_versionCache[key] = dcvi;
				}
				else
				{
					//There is no new version on server. So there is no need to change local version
				}
			}
			return dsNewer;
		}

		[System.ComponentModel.Description(CommonConstants.Messages.MethodIsInternal)]
		private static void StaticRemoveLocalCacheItem(string key)
		{
			try
			{
				_cache.Remove(key);
				//Will be deleted from version cache 
				DataCacheItemVersion dciv = null;
				_versionCache.TryRemove(key, out dciv);
			}
			catch (Exception exc)
			{
				string msg = String.Format("{0} {1}", "Cannot remove cache Item: '", key);
				EventLog.WriteEntry(CommonConstants.EventLogSource.Application, msg, EventLogEntryType.Error, CacheEventLogId.CacheCouldNotCleaned);
				throw new ApplicationException(msg, exc.InnerException);
			}
		}

		private static bool StaticHasCacheItem(string key)
		{
			if (_cache.GetCacheItem(key) != null)
				return true;
			else
				return false;
		}

		#endregion

		#region ICacheProvider Members

		public T GetCacheItem<T>(string key) where T : System.Data.DataSet
		{
			return (T)AppfabricCacheProvider.StaticGetCacheItem(key);
		}

		public DataSet GetCacheItem(string key)
		{
			return AppfabricCacheProvider.StaticGetCacheItem(key);
		}

		public DataSet GetCacheItemIfNewer(string key)
		{
			return AppfabricCacheProvider.StaticGetCacheItemIfNewer(key);
		}

		public void RefreshCacheItem(string key)
		{
			AppfabricCacheProvider.StaticRemoveLocalCacheItem(key);
			AppfabricCacheProvider.AppInsertCache(key, (CacheItem)_cacheItemHashTable[key]);
		}

		public void RemoveCacheItem(string key)
		{
			AppfabricCacheProvider.StaticRemoveLocalCacheItem(key);
		}

		public bool HasCacheItem(string key)
		{
			return StaticHasCacheItem(key);
		}


		#endregion
	}
}